Scala Cookbook翻译 Chapter 1.Strings 第二部分

1.5 遍历字符

  • 问题:你想遍历字符串中的每个字符,在遍历字符串时对每个字符进行操作。

    1.5.1 解决方案:

  • 根据你的需求和喜好,你可以使用map或者foreach方法,一个是循环,另一个是其他方法。

  • map把输入字符转成大写的例子

    scala> val upper = "hello, world".map(c => c.toUpper)
    upper: String = HELLO, WORLD
    
    //大部分使用以下缩写,使用下划线的魔法
    scala> val upper = "hello, world".map(_.toUpper)
    upper: String = HELLO, WORLD
    
  • 任何集合中,如字符串的字符序列,你可以把集合方法链接在一起以达到预期的结果。下面的例子是在原始字符串的基础上调用filter方法将所有的小写字母“L”删除后创建一个新的字符串,然后已这个新的字符串作为ma方法的输入将剩余的字符转换成大写字符。

    scala> val upper = "hello, world".filter(_ != 'l').map(_.toUpper)
    upper: String = HEO, WORD
    
  • 当你刚开始使用Scala时可能不大习惯map方法,使用Scala的for循环可以达到相同的结果。

    scala> for (c <- "hello") println(c)
    h 
    e 
    l 
    l 
    o       
    
  • 要写一个和map方法一样的for循环,可以在循环结束添加一个yield,此时的for/yield循环和前两个map方法例子是一样的。

    scala> val upper = for (c <- "hello, world") yield c.toUpper
    upper: String = HELLO, WORLD
    
  • 在for循环中添加yield本质上是将每次循环结果放置到暂存区,当循环结果,所有在暂存区的元素将作为一个集合返回。下例for/yield循环结果和第三个map方法一样。

    val result = for {
    c <- "hello, world"
    if c != 'l'
    } yield c.toUppe
    
  • map方法和for/yield方法用于将一个集合转换成另一个,而foreach方法通常用来操作每个元素而且没有返回值,下面这个打印的情况比较有用。

    scala> "hello".foreach(println)
    h 
    e 
    l 
    l
    o
    

1.5.2 讨论

  • 因为Scala把string当做字符序列,并且Scala是面向对象和函数式编程语言,你可以如上述方法显示的一样遍历字符串中的字符,下面使用经典Java方法进行比较。

    String s = "Hello";
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < s.length(); i++) {
        char c = s.charAt(i);
        // do something with the character ...
        // sb.append ...
    }
    String result = sb.toString();
    
  • 可以看到Scala方法更加简洁并且还是很可读的,这种简洁和可读使你更加专注于解决手上的问题,一旦你熟悉Scala,就像Java例子中的命令代码掩盖了业务逻辑。

  • 维基百科中对命令式编程的描述是这样的:命令式编程是一种编程范式,在改变程序状态的一系列声明中描述计算。命令程序定义计算机的命令序列来执行。这是Java的例子所示,它定义了一系列明确的声明告诉计算机如何达到预期的结果。

1.5.3 理解map如何工作:

  • 根据你的编码偏好,你可以在map方法中使用大的代码块。这两个例子证明了通过一个运算来传递一个map方法的语法。

    // first example
    "HELLO".map(c => (c.toByte+32).toChar)
    
    // second example
    "HELLO".map{ c =>
    (c.toByte+32).toChar
    }
    
  • 注意算法一次操作一个字符。因为这个例子中map方法被字符串调用,map将字符串当做一个字符元素的有序集合。map方法有一个隐式循环,并且在循环中一次操作一个字符运算。

  • 尽管这个运算很短,设想一下它需要很长的时间。在这种情况下,为了保持你的代码清晰,你可能想要把它写成一个方法(或函数),这个方法可以传递到map方法。要编写一个可以传递到map方法执行字符串中的字符的方法,定义它以一个单一的字符作为输入,然后执行该方法中的字符的逻辑。当逻辑完成时,返回任何运算返回的结果。尽管下面的运算依然很短,它演示了如何创建一个自定义方法,并将这个方法传递到map中。

    // 自定义操作字符的方法
    scala> def toLower(c: Char): Char = (c.toByte+32).toChar
    toLower: (c: Char)Char
    
    // 使用map方法
    scala> "HELLO".map(toLower)
    res0: String = hello
    
  • 作为额外的好处,同样的方法也可以使用在for/yield方法中。

    scala> val s = "HELLO"
    s: java.lang.String = HELLO
    
    scala> for (c <- s) yield toLower(c)
    res1: String = hello
    
  • 在讨论中我使用了“方法”一词,但是你也可以在这里使用函数代替方法,那么函数和方法的不同是什么?

    • 下面这个函数相当于toLower方法

      val toLower = (c: Char) => (c.toByte+32).toChar
      toLower: Char => Char = <function1>
      
    • 这个函数可以和之前使用的toLower方法以同样的方式传递到map中

      scala> "HELLO".map(toLower)
      res0: String = hello    
      
    • 详情可看第九章函数编程,查看更多函数信息和方法函数区别的。

1.5.4 完整的例子

  • 下面的例子演示如何在字符串中调用getBytes方法,然后传递代码块到foreach方法中来计算一个字符串的Adler-32校验和值。

    package tests
    
    /**
    * Calculate the Adler-32 checksum using Scala.
    * @see http://en.wikipedia.org/wiki/Adler-32
    */
    object Adler32Checksum {
        val MOD_ADLER = 65521
    
        def main(args: Array[String]) {
            val sum = adler32sum("Wikipedia")
            printf("checksum (int) = %d\n", sum)
            printf("checksum (hex) = %s\n", sum.toHexString)
        }
    
        def adler32sum(s: String): Int = {
            var a = 1
            var b = 0
            s.getBytes.foreach{char =>
                a = (char + a) % MOD_ADLER
                b = (b + a) % MOD_ADLER
            }
            // note: Int is 32 bits, which this requires
            b * 65536 + a // or (b << 16) + a
        }
    
    }
    
  • getBytes方法返回一个字符串的字节有序集合,如下所示:

    scala> "hello".getBytes
    res0: Array[Byte] = Array(104, 101, 108, 108, 111)
    
  • 调用getBytes之后添加foreach方法可以操作每个字节的值:

    scala> "hello".getBytes.foreach(println)
    104
    101
    108
    108
    111
    
  • 上面例子中使用foreach代替map,因为目标是循环字符串中的每个字节,并且在每个字节上进行一些操作,但是你并不想在循环的最后返回任何值。

  • 查看更多

    The Adler-32 checksum algorithm


1.6 字符串中的搜索模式

  • 问题:确定一个字符串中是否包含正则表达式模式

1.6.1 解决方案

  • 通过在字符串上调用.r方法创建一个正则表达式对象,当你查找一个匹配时使用findFirstIn模式,搜索所有匹配时用findAllIn。
  • 首先创建一个你想搜索的正则表达式模式,下面例子中是一个或多个数字字符的序列。

    //创建规则
    scala> val numPattern = "[0-9]+".r
    numPattern: scala.util.matching.Regex = [0-9]+
    
    //创建需要查找的字符串例子
    scala> val address = "123 Main Street Suite 101"
    address: java.lang.String = 123 Main Street Suite 101
    
    //使用findFirstIn方法查找第一个匹配(返回的是Option[String],讨论中详解)
    scala> val match1 = numPattern.findFirstIn(address)
    match1: Option[String] = Some(123)
    
    //使用findAllIn查找复杂匹配,返回迭代器
    scala> val matches = numPattern.findAllIn(address)
    matches: scala.util.matching.Regex.MatchIterator = non-empty iterator
    
    //循环输出结果
    scala> matches.foreach(println)
    123
    101
    
  • 如果findAllIn查找不到任何结果,就会返回一个空的迭代器,你不需要检查结果是否为空,所以你可以和之前一样写代码。如果你需要把结果输出成Array数组,可以再findAllIn之后调用toArray方法。

    scala> val matches = numPattern.findAllIn(address).toArray
    matches: Array[String] = Array(123, 101)
    
  • 如果没有匹配结果,这个方法会yield一个空的Array数组,其他方法如toList,toSeq,toVector也可用。

1.6.2 讨论

  • 在字符串之后使用.r方法比创建一个正则表达式对象要更加简单。另一个方法是导入Regex类,创建正则实例,然后如下方式使用实例:

    scala> import scala.util.matching.Regex
    import scala.util.matching.Regex
    
    scala> val numPattern = new Regex("[0-9]+")
    numPattern: scala.util.matching.Regex = [0-9]+
    
    scala> val address = "123 Main Street Suite 101"
    address: java.lang.String = 123 Main Street Suite 101
    
    scala> val match1 = numPattern.findFirstIn(address)
    match1: Option[String] = Some(123)
    
  • 尽管稍显繁琐,不过更加明显易懂。我发现很容易在字符串的后面忽略.r(然后花了几分钟想知道我看到的代码如何工作)

1.6.3 处理findFirstIn返回的Option

  • 如在解决方案中提到过的,findFirstIn方法查找字符串中的第一个匹配然后返回一个Option[String]:

    scala> val match1 = numPattern.findFirstIn(address)
    match1: Option[String] = Some(123)
    
  • 在章节20.6会详细说明Option/Some/None模式,简单的理解Option就是它是保存0个或1个值的容器。在这个findFirstIn的例子中,如果成功会返回字符串“123”如Some(123),如果查找失败会返回None:

    scala> val address = "No address given"
    address: String = No address given
    
    scala> val match1 = numPattern.findFirstIn(address)
    match1: Option[String] = None
    
  • 总结就是,一个定义的方法返回的Option[String]要不返回的是Some(String),要么是None。正常使用Option的方式一般如下:

    • 在结果后调用getOrElse
    • 在匹配表达式中使用Option
    • 在foreach循环中使用Option
  • 章节20.6会详细描述这些方法,为了方便这里会简单介绍下。

  • 使用getOrElse方法,会试图“get”到结果,同时还指定当方法失败时应使用的默认值:

    scala> val result = numPattern.findFirstIn(address).getOrElse("no match")
    result: String = 123
    
  • 因为Option是0个或1个元素的集合,一个有经验的Scala开发者会在这种情况下同时使用foreach循环。

    numPattern.findFirstIn(address).foreach { e =>
        // perform the next step in your algorithm,
        // operating on the value 'e'
    }
    
  • 匹配表达式也为这个问题提供了一个非常可读的解决方案,更多查看20.6章。

    match1 match {
        case Some(s) => println(s"Found: $s")
        case None =>
    }
    
  • 对这个方法进行总结,下面的REPL例子显示了创建正则表达式,通过findFirstIn搜索字符串,在匹配结果使用foreach循环的完整结果。

    scala> val numPattern = "[0-9]+".r
    numPattern: scala.util.matching.Regex = [0-9]+
    
    scala> val address = "123 Main Street Suite 101"
    address: String = 123 Main Street Suite 101
    
    scala> val match1 = numPattern.findFirstIn(address)
    match1: Option[String] = Some(123)
    
    scala> match1.foreach { e =>
        | println(s"Found a match: $e")
        | }
    Found a match: 123
    
  • 查看更多

    The StringOps class
    The Regex class


1.7 字符串中的替换模式

  • 问题:想要在字符串中搜索一个正则表达式模式并替换他们。

    1.7.1 解决方案

  • 因为字符串是不可改变的,你不能直接在字符串的基础上进行查找并替换的操作。但是你可以创建一个包含被替换内容的新字符串。有以下几种方式做到这个。
  • 可以在字符串上调用replaceAll,记住将结果分配给一个新的变量。

    scala> val address = "123 Main Street".replaceAll("[0-9]", "x")
    address: java.lang.String = xxx Main Street
    
  • 可以创建正则表达式然后在表达式基础上调用replaceAllIn,仍然需要记住将结果分配给一个新的变量。

    scala> val regex = "[0-9]".r
    regex: scala.util.matching.Regex = [0-9]
    
    scala> val newAddress = regex.replaceAllIn("123 Main Street", "x")
    newAddress: String = xxx Main Street
    
  • 只替换第一个匹配模式的,使用replaceFirst方法:

    scala> val result = "123".replaceFirst("[0-9]", "x")
    result: java.lang.String = x23
    
  • 也可以使用一个正则表达式的replaceFirstIn:

    scala> val regex = "H".r
    regex: scala.util.matching.Regex = H
    
    scala> val result = regex.replaceFirstIn("Hello world", "J")
    result: String = Jello world